package com.only4play.portal.common.util.http.apache;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
import org.apache.hc.client5.http.HttpRequestRetryStrategy;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author liyuncong
 * @version 1.0
 * @file DefaultApacheHttpClientBuilder
 * @brief DefaultApacheHttpClientBuilder
 * @details DefaultApacheHttpClientBuilder
 * @date 2024-06-12
 *
 * Edit History
 * ----------------------------------------------------------------------------
 * DATE                     NAME               DESCRIPTION
 * 2024-06-12               liyuncong          Created
 */

@Slf4j
public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {

    /**
     * Apache HttpClient builder prepare
     */
    private final AtomicBoolean prepared = new AtomicBoolean(false);

    /**
     * Request connect timeout
     * 0: Always waiting
     * -1: Use httpClient default configuration
     */
    private int connectionRequestTimeout = -1;

    private int connectionTimeout = 5000;

    private Timeout soTimeout = Timeout.of(5000L, TimeUnit.MILLISECONDS);

    /**
     * Idle connection timeout
     */
    private int idleConnTimeout = 60000;

    /**
     *
     */
    private int checkWaitTime = 60000;

    /**
     * Maximum number of links per route, default to 10
     */
    private int maxConnPerRoute = 10;

    /**
     * Maximum number of connection, default to 10
     */
    private int maxTotalConn = 10;

    /**
     * Custom httpclient userAgent
     */
    private String userAgent;

    /**
     * Custom httpclient httpRequestRetryStrategy
     */
    private HttpRequestRetryStrategy httpRequestRetryStrategy;

    /**
     * Custom httpclient connectionKeepAliveStrategy
     */
    private ConnectionKeepAliveStrategy connectionKeepAliveStrategy;

    private SSLConnectionSocketFactory sslConnectionSocketFactory = SSLConnectionSocketFactory.getSocketFactory();
    private final PlainConnectionSocketFactory plainConnectionSocketFactory = PlainConnectionSocketFactory.getSocketFactory();
    private final HttpRequestRetryStrategy defaultHttpRequestRetryStrategy = new DefaultHttpRequestRetryStrategy();
    private String httpProxyHost;
    private int httpProxyPort;
    private String httpProxyUsername;
    private String httpProxyPassword;

    /**
     * Idle connection monitor thread
     */
    private IdleConnectionMonitorThread idleConnectionMonitorThread;

    /**
     * Hold the client object and initialize it only once to avoid duplicate
     * initialization issues when multiple service instances are present
     */
    private CloseableHttpClient closeableHttpClient;

    private DefaultApacheHttpClientBuilder() {

    }

    public static DefaultApacheHttpClientBuilder getInstance() {
        return DefaultApacheHttpClientBuilder.SingletonHolder.INSTANCE;
    }

    private static class SingletonHolder {
        private static final DefaultApacheHttpClientBuilder INSTANCE = new DefaultApacheHttpClientBuilder();
    }

    @Override
    public CloseableHttpClient build() {
        if (!prepared.get()) {
            prepare();
        }
        return this.closeableHttpClient;
    }

    @Override
    public ApacheHttpClientBuilder httpProxyHost(String httpProxyHost) {
        this.httpProxyHost = httpProxyHost;
        return this;
    }

    @Override
    public ApacheHttpClientBuilder httpProxyPort(int httpProxyPort) {
        this.httpProxyPort = httpProxyPort;
        return this;
    }

    @Override
    public ApacheHttpClientBuilder httpProxyUsername(String httpProxyUsername) {
        this.httpProxyUsername = httpProxyUsername;
        return this;
    }

    @Override
    public ApacheHttpClientBuilder httpProxyPassword(String httpProxyPassword) {
        this.httpProxyPassword = httpProxyPassword;
        return this;
    }

    @Override
    public ApacheHttpClientBuilder httpRequestRetryStrategy(HttpRequestRetryStrategy httpRequestRetryStrategy) {
        this.httpRequestRetryStrategy = httpRequestRetryStrategy;
        return this;
    }

    @Override
    public ApacheHttpClientBuilder keepAliveStrategy(ConnectionKeepAliveStrategy keepAliveStrategy) {
        this.connectionKeepAliveStrategy = keepAliveStrategy;
        return this;
    }

    @Override
    public ApacheHttpClientBuilder sslConnectionSocketFactory(SSLConnectionSocketFactory sslConnectionSocketFactory) {
        this.sslConnectionSocketFactory = sslConnectionSocketFactory;
        return this;
    }

    private void prepare() {
        if (prepared.get()) {
            return;
        }

        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
            .register("http", this.plainConnectionSocketFactory)
            .register("https", this.plainConnectionSocketFactory)
            .build();

        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
        connectionManager.setMaxTotal(this.maxTotalConn);
        connectionManager.setDefaultMaxPerRoute(this.maxConnPerRoute);
        connectionManager.setDefaultSocketConfig(SocketConfig.copy(SocketConfig.DEFAULT).setSoTimeout(soTimeout).build());

        this.idleConnectionMonitorThread = new IdleConnectionMonitorThread(
            connectionManager, this.idleConnTimeout, this.checkWaitTime);
        this.idleConnectionMonitorThread.setDaemon(true);
        this.idleConnectionMonitorThread.start();

        HttpClientBuilder httpClientBuilder = HttpClients.custom()
            .setConnectionManager(connectionManager)
            .setConnectionManagerShared(true)
            .setDefaultRequestConfig(RequestConfig.custom()
                .setConnectionRequestTimeout(this.connectionRequestTimeout, TimeUnit.MILLISECONDS)
                .build()
            );

        httpRequestRetryStrategy = httpRequestRetryStrategy == null ? defaultHttpRequestRetryStrategy : httpRequestRetryStrategy;
        httpClientBuilder.setRetryStrategy(httpRequestRetryStrategy);

        if (connectionKeepAliveStrategy != null) {
            httpClientBuilder.setKeepAliveStrategy(connectionKeepAliveStrategy);
        }

        if (StringUtils.isNotBlank(this.httpProxyHost) && StringUtils.isNotBlank(this.httpProxyUsername)) {
            // Use proxy server, auth user
            BasicCredentialsProvider provider = new BasicCredentialsProvider();
            provider.setCredentials(new AuthScope(this.httpProxyHost, this.httpProxyPort),
                new UsernamePasswordCredentials(this.httpProxyUsername, this.httpProxyPassword.toCharArray()));
            httpClientBuilder.setDefaultCredentialsProvider(provider);
            httpClientBuilder.setProxy(new HttpHost(this.httpProxyHost, this.httpProxyPort));
        }

        if (StringUtils.isNotBlank(this.userAgent)) {
            httpClientBuilder.setUserAgent(userAgent);
        }

        this.closeableHttpClient = httpClientBuilder.build();
        prepared.set(true);
    }


    public static class IdleConnectionMonitorThread extends Thread {
        private final PoolingHttpClientConnectionManager connectionManager;
        private final int idleConnTimeout;
        private final int checkWaitTime;
        private volatile boolean shutdown;

        public IdleConnectionMonitorThread(PoolingHttpClientConnectionManager connectionManager, int idleConnTimeout, int checkWaitTime) {
            super("IdleConnectionMonitorThread");
            this.connectionManager = connectionManager;
            this.idleConnTimeout = idleConnTimeout;
            this.checkWaitTime = checkWaitTime;
        }

        @Override
        public void run() {
            try {
                while (!this.shutdown) {
                    synchronized (this) {
                        wait(this.checkWaitTime);
                        this.connectionManager.closeExpired();
                        this.connectionManager.closeIdle(TimeValue.of(this.idleConnTimeout, TimeUnit.MILLISECONDS));
                    }
                }
            } catch (InterruptedException ignored) {

            }
        }

        public void trigger() {
            synchronized (this) {
                notifyAll();
            }
        }

        public void shutdown() {
            this.shutdown = true;
            synchronized (this) {
                notifyAll();
            }
        }
    }
}
